// src/main/java/com/dbms/storage/BPlusTreeLeafPage.java
package com.dbms.storage;

import com.dbms.common.RID;
import java.nio.ByteBuffer;

/**
 * B+树的叶子节点实现类，继承自BPlusTreePage。
 * 核心作用：存储「键-记录标识符(RID)」键值对，是B+树的最终数据节点；
 * 叶子节点之间通过双向链表连接（前向/后向指针），支持范围查询和顺序访问。
 *
 * 叶子节点页面布局（总大小通常为4096字节，具体取决于页大小定义）：
 * ----------------------------------------------------------------------------------
 * | 头部信息(HEADER, 16字节) | 下一页ID(4字节) | 上一页ID(4字节) | 键值对数据...       |
 * ----------------------------------------------------------------------------------
 * 头部信息：继承自BPlusTreePage，包含当前大小(size)、最大容量(maxSize)、页面类型等元数据；
 * 下一页ID/上一页ID：维护叶子节点的双向链表，支持顺序遍历；
 * 键值对数据：存储若干「key-RID」对，按key升序排列。
 */
public class BPlusTreeLeafPage extends BPlusTreePage {

    /** 下一页ID的偏移量：紧跟在头部信息之后（头部大小HEADER_SIZE由父类定义） */
    private static final int NEXT_PAGE_ID_OFFSET = HEADER_SIZE;
    /** 上一页ID的偏移量：在下一页ID之后（占4字节） */
    private static final int PREV_PAGE_ID_OFFSET = HEADER_SIZE + 4;
    /** 键值对内容的起始偏移量：在头部、下一页ID、上一页ID之后（16+4+4=24字节） */
    private static final int CONTENT_OFFSET = HEADER_SIZE + 8;

    // 数据单元大小定义（用于计算偏移和分配空间）
    /** 单个key的字节大小：Integer类型占4字节 */
    private static final int KEY_SIZE = 4;
    /** RID中页ID的字节大小：int类型占4字节 */
    private static final int RID_PAGE_ID_SIZE = 4;
    /** RID中槽位号的字节大小：int类型占4字节 */
    private static final int RID_SLOT_NUM_SIZE = 4;
    /** 单个RID的总字节大小：页ID(4) + 槽位号(4) = 8字节 */
    private static final int RID_SIZE = RID_PAGE_ID_SIZE + RID_SLOT_NUM_SIZE;
    /** 单个「key-RID」键值对的总字节大小：key(4) + RID(8) = 12字节 */
    private static final int PAIR_SIZE = KEY_SIZE + RID_SIZE;

    /**
     * 构造方法：将底层Page对象包装为B+树叶子节点页面
     * @param page 底层存储页面（包含原始字节数据）
     */
    public BPlusTreeLeafPage(Page page) {
        this.page = page; // 赋值父类的page成员变量
        this.buffer = ByteBuffer.wrap(page.getData()); // 包装页面字节数据为ByteBuffer，便于随机读写
    }

    /**
     * 初始化叶子节点页面的元数据
     * @param maxSize 叶子节点的最大容量（最大可存储的「key-RID」对数量）
     */
    public void init(int maxSize) {
        // 调用父类BPlusTreePage的init方法，初始化头部信息（size=0、maxSize、页面类型=LEAF_PAGE等）
        super.init(page, maxSize, PageType.LEAF_PAGE);
        // 初始化双向链表指针：默认下一页和上一页均为无效页ID
        setNextPageId(INVALID_PAGE_ID);
        setPrevPageId(INVALID_PAGE_ID);
    }

    // --- 双向链表指针操作（维护叶子节点间的顺序关系） ---

    /**
     * 获取当前叶子节点的下一个兄弟节点的页ID
     * @return 下一页ID（INVALID_PAGE_ID表示无下一页）
     */
    public int getNextPageId() {
        return buffer.getInt(NEXT_PAGE_ID_OFFSET);
    }

    /**
     * 设置当前叶子节点的下一个兄弟节点的页ID，并标记页面为脏页
     * @param nextPageId 下一个兄弟节点的页ID
     */
    public void setNextPageId(int nextPageId) {
        buffer.putInt(NEXT_PAGE_ID_OFFSET, nextPageId);
        markDirty(); // 标记页面为脏页（修改需刷盘）
    }

    /**
     * 获取当前叶子节点的上一个兄弟节点的页ID
     * @return 上一页ID（INVALID_PAGE_ID表示无上一页）
     */
    public int getPrevPageId() {
        return buffer.getInt(PREV_PAGE_ID_OFFSET);
    }

    /**
     * 设置当前叶子节点的上一个兄弟节点的页ID，并标记页面为脏页
     * @param prevPageId 上一个兄弟节点的页ID
     */
    public void setPrevPageId(int prevPageId) {
        buffer.putInt(PREV_PAGE_ID_OFFSET, prevPageId);
        markDirty(); // 标记页面为脏页
    }

    // --- 键值对访问与操作 ---

    /**
     * 二分查找目标键在当前叶子节点中的索引（核心查找逻辑）
     * 作用：1. 若键存在，返回其索引；2. 若键不存在，返回应插入的位置（保持键有序）
     * @param key 目标查找键
     * @return 键的索引（存在时）或插入位置（不存在时）
     */
    @Override
    public int keyIndex(Integer key) {
        int low = 0; // 查找范围起始索引
        int high = getSize() - 1; // 查找范围结束索引（当前节点存储的键值对数量-1）

        // 二分查找循环
        while (low <= high) {
            int mid = low + (high - low) / 2; // 计算中间索引（避免溢出）
            int cmp = getKey(mid).compareTo(key); // 比较中间键与目标键

            if (cmp < 0) {
                // 中间键小于目标键，目标键在右半区
                low = mid + 1;
            } else if (cmp > 0) {
                // 中间键大于目标键，目标键在左半区
                high = mid - 1;
            } else {
                // 中间键等于目标键，找到匹配索引
                return mid;
            }
        }
        // 循环结束后，low即为键不存在时的插入位置（保持升序）
        return low;
    }

    /**
     * 根据索引获取叶子节点中的键
     * @param index 键的索引（0<=index<size）
     * @return 索引对应的键
     */
    @Override
    public Integer getKey(int index) {
        // 计算键在字节缓冲区中的偏移：内容区起始 + 索引*键值对大小（每个对占PAIR_SIZE字节）
        return buffer.getInt(CONTENT_OFFSET + index * PAIR_SIZE);
    }

    /**
     * 根据索引获取叶子节点中的记录标识符（RID）
     * @param index RID的索引（0<=index<size，与键的索引一一对应）
     * @return 索引对应的RID（包含页ID和槽位号）
     */
    public RID getRid(int index) {
        // 计算RID中页ID的偏移：内容区起始 + 索引*键值对大小 + 键的大小（键在前，RID在后）
        int pageId = buffer.getInt(CONTENT_OFFSET + index * PAIR_SIZE + KEY_SIZE);
        // 计算RID中槽位号的偏移：页ID偏移 + RID_PAGE_ID_SIZE（4字节）
        int slotNum = buffer.getInt(CONTENT_OFFSET + index * PAIR_SIZE + KEY_SIZE + RID_PAGE_ID_SIZE);
        return new RID(pageId, slotNum); // 构造并返回RID对象
    }

    /**
     * 在指定索引位置插入一个「key-RID」键值对（保持键有序）
     * 逻辑：1. 移动索引及之后的键值对，腾出位置 2. 插入新键值对 3. 增加节点大小
     * @param index 插入位置的索引（0<=index<=size）
     * @param key 要插入的键
     * @param rid 要插入的键对应的RID
     */
    public void insertAt(int index, Integer key, RID rid) {
        // 1. 计算需要移动的键值对数量：当前大小 - 插入索引（index及之后的都需后移）
        int numToMove = getSize() - index;
        if (numToMove > 0) {
            // 移动数据：从index位置开始的numToMove个键值对，向后移动一个PAIR_SIZE的位置
            System.arraycopy(
                    page.getData(), CONTENT_OFFSET + index * PAIR_SIZE, // 源数据起始位置
                    page.getData(), CONTENT_OFFSET + (index + 1) * PAIR_SIZE, // 目标位置（后移一位）
                    numToMove * PAIR_SIZE // 移动的总字节数
            );
        }

        // 2. 在腾出的index位置插入新键值对
        // 写入key
        buffer.putInt(CONTENT_OFFSET + index * PAIR_SIZE, key);
        // 写入RID的页ID
        buffer.putInt(CONTENT_OFFSET + index * PAIR_SIZE + KEY_SIZE, rid.getPageId());
        // 写入RID的槽位号
        buffer.putInt(CONTENT_OFFSET + index * PAIR_SIZE + KEY_SIZE + RID_PAGE_ID_SIZE, rid.getSlotNum());

        // 3. 增加节点的当前大小（插入了一个键值对）
        increaseSize(1);
        // 注：increaseSize内部已调用markDirty()，无需重复标记
    }

    /**
     * 【新增】将当前叶子节点的后半部分数据移动到新叶子节点（用于节点分裂）
     * 场景：当前节点已满（size == maxSize），分裂为两个节点，均衡存储数据
     * @param recipient 接收数据的新叶子节点（空节点）
     */
    public void moveHalfTo(BPlusTreeLeafPage recipient) {
        // 1. 计算分裂起始位置：maxSize / 2（前半部分保留在当前节点，后半部分移动到新节点）
        // 例：maxSize=4 → 起始位置=2，当前节点保留0-1，新节点接收2-3
        int start = getMaxSize() / 2;
        // 2. 计算需要移动的键值对数量：当前节点总大小 - 起始位置
        int numToMove = getSize() - start;

        // 3. 将当前节点从start位置开始的numToMove个键值对，拷贝到新节点
        recipient.copyNFrom(page.getData(), CONTENT_OFFSET + start * PAIR_SIZE, numToMove);

        // 4. 更新当前节点的大小：保留前start个键值对
        setSize(start);
        // 注：setSize内部已调用markDirty()
    }

    /**
     * 【新增】从源字节数据中拷贝指定数量的「key-RID」对到当前节点（供moveHalfTo调用）
     * 作用：接收从分裂节点移动过来的键值对数据
     * @param source 源字节数组（来自分裂节点的页面数据）
     * @param offset 源数据中要拷贝的起始偏移量
     * @param n 要拷贝的键值对数量
     */
    public void copyNFrom(byte[] source, int offset, int n) {
        // 当前节点的当前大小（拷贝前的初始大小，通常为0，因recipient是新节点）
        int currentSize = getSize();
        // 拷贝逻辑：将源数据中n个键值对，从offset开始，拷贝到当前节点的CONTENT_OFFSET + currentSize * PAIR_SIZE位置
        System.arraycopy(
                source, offset, // 源数据及起始偏移
                page.getData(), CONTENT_OFFSET + currentSize * PAIR_SIZE, // 目标位置（当前节点已有数据之后）
                n * PAIR_SIZE // 拷贝的总字节数（n个键值对，每个PAIR_SIZE字节）
        );
        // 增加当前节点的大小：拷贝了n个键值对，size += n
        increaseSize(n);
        // 注：increaseSize内部已调用markDirty()
    }

    /**
     * 【新增】移除指定索引位置的「key-RID」键值对
     * 逻辑：1. 移动索引之后的键值对，覆盖被删除位置 2. 减小节点大小
     * @param index 要移除的键值对的索引（0<=index<size）
     */
    public void removeAt(int index) {
        // 1. 计算需要移动的键值对数量：当前大小 - 索引 - 1（index之后的所有键值对）
        int numToMove = getSize() - index - 1;
        if (numToMove > 0) {
            // 移动数据：从index+1位置开始的numToMove个键值对，向前移动一个PAIR_SIZE的位置（覆盖被删除位置）
            System.arraycopy(
                    page.getData(), CONTENT_OFFSET + (index + 1) * PAIR_SIZE, // 源数据起始位置
                    page.getData(), CONTENT_OFFSET + index * PAIR_SIZE, // 目标位置（前移一位）
                    numToMove * PAIR_SIZE // 移动的总字节数
            );
        }

        // 2. 减小节点的当前大小（删除了一个键值对）
        increaseSize(-1);
        // 标记页面为脏页（修改需刷盘）
        markDirty();
    }
}